<?php
/**
 * XCaptchaAction renders a CAPTCHA image.
 *
 * Note, XCaptchaAction requires PHP GD2 extension.
 *
 * @author Qiang Xue <qiang.xue@gmail.com>
 * @version $Id: XCaptchaAction.php 72 2010-12-13 02:11:40Z mole1230 $
 */
class XCaptchaAction extends CAction
{
	/**
	 * Prefix to the session variable name used by the action.
	 */
	const SESSION_VAR_PREFIX = 'Yii.XCaptchaAction.';

	/**
	 * @var integer the width of the generated CAPTCHA image. Defaults to 120.
	 */
	public $width = 120;
	
	/**
	 * @var integer the height of the generated CAPTCHA image. Defaults to 50.
	 */
	public $height = 50;
	
	/**
	 * @var integer padding around the text. Defaults to 2.
	 */
	public $padding = 2;
	
	/**
	 * @var integer the background color. For example, 0x55FF00.
	 * Defaults to 0xFFFFFF, meaning white color.
	 */
	public $backColor = 0xFFFFFF;
	
	/**
	 * @var integer the font color. For example, 0x55FF00. Defaults to 0x2040A0 (blue color).
	 */
	public $foreColor = 0x2040A0;
	
	/**
	 * @var boolean whether to use transparent background. Defaults to false.
	 * @since 1.0.10
	 */
	public $transparent = false;
	
	/**
	 * @var integer the minimum length for randomly generated word. Defaults to 6.
	 */
	public $length = 4;

	/**
	 * @var string the TrueType font file. Defaults to Duality.ttf which is provided
	 * with the Yii release.
	 */
	public $fontFile;
	
	/**
	 * @var string the fixed verification code. When this is property is set,
	 * {@link getVerifyCode} will always return this value.
	 * This is mainly used in automated tests where we want to be able to reproduce
	 * the same verification code each time we run the tests.
	 * Defaults to null, meaning the verification code will be randomly generated.
	 * @since 1.1.4
	 */
	public $fixedVerifyCode;
	
	/**
	 * Runs the action.
	 * If the GET parameter {@link wsdlVar} exists, the action will serve WSDL content;
	 * If not, the action will handle the remote method invocation.
	 */
	public function run()
	{
		$this->renderImage($this->getVerifyCode(true));
		Yii::app()->end();
	}
	
	/**
	 * Gets the verification code.
	 * @param string whether the verification code should be regenerated.
	 * @return string the verification code.
	 */
	public function getVerifyCode($regenerate = false)
	{
		if ($this->fixedVerifyCode !== null)
			return $this->fixedVerifyCode;
		
		$session = Yii::app()->session;
		$session->open();
		$name = $this->getSessionKey();
		if ($session[$name] === null || $regenerate) {
			$session[$name] = $this->generateVerifyCode();
		}
		return $session[$name];
	}
	
	/**
	 * Validates the input to see if it matches the generated code.
	 * @param string user input
	 * @param boolean whether the comparison should be case-sensitive
	 * @return whether the input is valid
	 */
	public function validate($input, $caseSensitive)
	{
		$code = $this->getVerifyCode();
		$valid = $caseSensitive ? ($input === $code) : !strcasecmp($input, $code);
		$session = Yii::app()->session;
		$session->open();
		unset($session[$this->getSessionKey()]);
		return $valid;
	}
	
	/**
	 * Generates a new verification code.
	 * @return string the generated verification code
	 */
	protected function generateVerifyCode()
	{
		$length = $this->length;
		$letters = 'bcdfghjklmnpqrstvwxyz';
		$vowels = 'aeiou';
		$code = '';
		for($i = 0; $i < $length; ++$i) {
			if ($i % 2 && rand(0, 10) > 2 || !($i % 2) && rand(0, 10) > 9)
				$code .= $vowels[rand(0, 4)];
			else
				$code .= $letters[rand(0, 20)];
		}
		
		return $code;
	}
	
	/**
	 * Returns the session variable name used to store verification code.
	 * @return string the session variable name
	 */
	protected function getSessionKey()
	{
		return self::SESSION_VAR_PREFIX . Yii::app()->getId() . '.' . $this->getController()->getUniqueId() . '.' . $this->getId();
	}
	
	/**
	 * Renders the CAPTCHA image based on the code.
	 * @param string the verification code
	 * @return string image content
	 */
	protected function renderImage($code)
	{
		$image = imagecreatetruecolor($this->width, $this->height);
		
		$backColor = imagecolorallocate($image, (int) ($this->backColor % 0x1000000 / 0x10000), (int) ($this->backColor % 0x10000 / 0x100), $this->backColor % 0x100);
		imagefilledrectangle($image, 0, 0, $this->width, $this->height, $backColor);
		imagecolordeallocate($image, $backColor);
		
		if ($this->transparent)
			imagecolortransparent($image, $backColor);
		
		$foreColor = imagecolorallocate($image, (int) ($this->foreColor % 0x1000000 / 0x10000), (int) ($this->foreColor % 0x10000 / 0x100), $this->foreColor % 0x100);
		
		if ($this->fontFile === null)
			$this->fontFile = dirname(__FILE__) . '/Duality.ttf';
		
		$offset = 2;
		$length = strlen($code);
		$box = imagettfbbox(30, 0, $this->fontFile, $code);
		$w = $box[4] - $box[0] - $offset * ($length - 1);
		$h = $box[1] - $box[5];
		$scale = min(($this->width - $this->padding * 2) / $w, ($this->height - $this->padding * 2) / $h);
		$x = 10;
		$y = round($this->height * 27 / 40);
		for($i = 0; $i < $length; ++$i) {
			$fontSize = (int) (rand(26, 32) * $scale * 0.8);
			$angle = rand(-10, 10);
			$letter = $code[$i];
			$box = imagettftext($image, $fontSize, $angle, $x, $y, $foreColor, $this->fontFile, $letter);
			$x = $box[2] - $offset;
		}
		
		imagecolordeallocate($image, $foreColor);
		
		header('Pragma: public');
		header('Expires: 0');
		header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
		header('Content-Transfer-Encoding: binary');
		header("Content-type: image/png");
		imagepng($image);
		imagedestroy($image);
	}
}
